package com.pai4j.pgc.service.community;
import com.google.common.collect.Lists;
import com.pai4j.common.bean.PAIPageResponseBeanUtil;
import com.pai4j.common.enums.RedisKeyEnum;
import com.pai4j.common.enums.community.*;
import com.pai4j.common.enums.CommonStatusEnum;
import com.pai4j.common.exception.BusinessException;
import com.pai4j.common.listener.event.CommunityContentEvent;
import com.pai4j.common.listener.event.data.CommunityEventData;
import com.pai4j.common.util.*;
import com.pai4j.common.util.community.ArticleUtil;
import com.pai4j.common.util.community.HtmlToMarkdownConverter;
import com.pai4j.pgc.pojo.es.DtoRequest;
import com.pai4j.domain.community.vo.request.CCExperienceReviewRequestVO;
import com.pai4j.domain.community.vo.request.CCExperienceSaveRequestVO;
import com.pai4j.domain.community.vo.response.admin.DataCountResponseVO;
import com.pai4j.domain.community.vo.response.cc.CTCExperienceBaseVO;
import com.pai4j.domain.community.vo.response.cc.CTCExperienceDetailVO;
import com.pai4j.domain.community.vo.response.cc.CTCExperienceListVO;
import com.pai4j.domain.community.vo.response.cc.CTCPathDetailVO;
import com.pai4j.domain.community.vo.response.message.MessageResourceVO;
import com.pai4j.domain.vo.response.ResourceBaseInfoVO;
import com.pai4j.domain.vo.response.UserBaseResponseInfoVO;
import com.pai4j.pgc.dao.community.ICCExperienceEntityDAO;
import com.pai4j.pgc.dao.community.ICoderUtilUserDAO;
import com.pai4j.pgc.entity.community.CCExperienceEntity;
import com.pai4j.pgc.entity.community.UserEntity;
import com.pai4j.pgc.service.behavior.AbstractResourceBehaviorService;
import com.pai4j.pgc.service.behavior.ResourceBehaviorFactory;
import com.pai4j.pgc.service.event.CourseStudyEvent;
import com.pai4j.remote.user.UserServiceClient;
import jakarta.persistence.criteria.CriteriaBuilder;
import jakarta.persistence.criteria.CriteriaQuery;
import jakarta.persistence.criteria.Predicate;
import jakarta.persistence.criteria.Root;
import org.apache.commons.collections.CollectionUtils;
import org.apache.commons.lang3.ObjectUtils;
import org.apache.commons.lang3.StringUtils;
import org.springframework.beans.BeanUtils;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.context.ApplicationContext;
import org.springframework.data.domain.Page;
import org.springframework.data.domain.PageRequest;
import org.springframework.data.domain.Pageable;
import org.springframework.data.domain.Sort;
import org.springframework.data.jpa.domain.Specification;
import org.springframework.data.redis.core.DefaultTypedTuple;
import org.springframework.data.redis.core.ZSetOperations;
import org.springframework.stereotype.Service;
import org.springframework.util.Assert;
import java.util.*;
import java.util.stream.Collectors;

/**
 * 技术社区经验/文章服务
 *
 * @author: CYM-pai
 * @date: 2025/10/27 14:24
 **/
@Service
public class CCExperienceService implements ResourceMessageInterface, CommonInterface {

    @Autowired
    private ICCExperienceEntityDAO experienceEntityDAO;
    @Autowired
    private RedisUtil redisUtil;
    @Autowired
    private UserServiceClient userServiceClient;
    @Autowired
    private CCPathService pathService;
    @Autowired
    private CTCScheduleService scheduleService;
    @Autowired
    private ICoderUtilUserDAO userDAO;
    @Autowired
    private ApplicationContext applicationContext;

    /**
     *  经验/文章发布
     *
     * @author: CYM-pai
     * @date: 2025/10/28 20:25
     **/
    public Long save(CCExperienceSaveRequestVO experienceSaveRequest, String userId) {
        CCExperienceEntity experienceEntity = this.convert(experienceSaveRequest, userId);
        experienceEntity = experienceEntityDAO.save(experienceEntity);
        CCExperienceEntity finalExperienceEntity = experienceEntity;
        ThreadPoolExecutorUtil.execute(() -> this.refreshExperienceBaseCache(finalExperienceEntity));
        return experienceEntity.getId();
    }

    public boolean delete(Long experienceId, String userId) {
        CCExperienceEntity exp = experienceEntityDAO.findById(experienceId).orElse(null);
        Assert.isTrue(exp != null, "经验不存在！");
        Assert.isTrue(exp.getStatus().equals(CommonStatusEnum.PUBLISHED.getStatus()), "经验未发布！");
        boolean hasDeleteAuth = userServiceClient.isAdmin(userId) || exp.getAuthor().equals(userId);
        Assert.isTrue(hasDeleteAuth, "无删除权限！");
        exp.setStatus(CommonStatusEnum.DELETED.getStatus());
        exp.setUpdateBy(userId);
        exp.setUpdateDate(new Date());
        experienceEntityDAO.save(exp);
        // 刷新缓存
        ThreadPoolExecutorUtil.execute(() -> this.deleteExperienceByCache(exp.getPathId(), experienceId));
        return true;
    }


    public Set<String> getCCPathHotExperienceSignsFromCache(Long pathId, int size) {
        String key = RedisKeyEnum.CTC_EXPERIENCE_HOT_SIGNS_CACHE.getKey(pathId.toString());
        return redisUtil.zreverseRange(key, 0, size);
    }

    public List<CTCExperienceListVO> getRecommendList(Long pathId) {
        return scheduleService.getHomeExperienceFromCache(pathId);
    }

    /**
     * 刷新专区的热门标签
     * @param pathId
     */
    public void refreshCCPathHotExperienceSignsCache(Long pathId) {
        List<String> signs = experienceEntityDAO.findAllSignsLimit100ByPathId(pathId);
        if (CollectionUtils.isEmpty(signs)) {
            return;
        }
        String key = RedisKeyEnum.CTC_EXPERIENCE_HOT_SIGNS_CACHE.getKey(pathId.toString());
        Map<String, Long> signCountMap = new HashMap<>();
        for (String signStr : signs) {
            String[] signArr = signStr.split(",");
            for (String sign : signArr) {
                Long count = signCountMap.getOrDefault(sign, 0L);
                signCountMap.put(sign, ++count);
            }
        }
        redisUtil.remove(key);
        for (Map.Entry<String, Long> entry : signCountMap.entrySet()) {
            String val = entry.getKey();
            Long score = entry.getValue();
            redisUtil.zadd(key, val, score, RedisKeyEnum.CTC_EXPERIENCE_HOT_SIGNS_CACHE.getExpireTime());
        }
    }

    /**
     * 设置经验置顶
     * @param id
     * @param status
     * @param userId
     */
    public void setTop(Long id, Boolean status, String userId) {
        CCExperienceEntity experienceEntity = experienceEntityDAO.findById(id).orElse(null);
        Assert.isTrue(experienceEntity != null, "操作失败:经验不存在!");
        experienceEntity.setTop(status);
        experienceEntity.setTopDate(status ? new Date() : null);
        experienceEntity.setUpdateBy(userId);
        experienceEntity.setUpdateDate(new Date());
        experienceEntity = experienceEntityDAO.save(experienceEntity);
        /**
         * 刷新置顶经验缓存
         */
        this.refreshExperienceBaseCache(id);
        this.setExperienceTopListCache(experienceEntity.getPathId(), id, status);
    }

    /**
     * 设置经验推荐
     * @param id
     * @param status
     * @param userId
     */
    public void setRecommend(Long id, Boolean status, String userId) {
        CCExperienceEntity experienceEntity = experienceEntityDAO.findById(id).orElse(null);
        Assert.isTrue(experienceEntity != null, "操作失败:经验不存在!");
        experienceEntity.setRecommend(status);
        experienceEntity.setRecommendDate(status ? new Date() : null);
        experienceEntity.setUpdateBy(userId);
        experienceEntity.setUpdateDate(new Date());
        experienceEntity = experienceEntityDAO.save(experienceEntity);
        /**
         * 刷新置顶经验缓存
         */
        this.refreshExperienceBaseCache(id);
        this.setExperienceRecommendListCache(experienceEntity.getPathId(), id, status);
    }

    /**
     * 设置置顶经验缓存
     * @param pathId
     * @param id
     * @param status
     */
    private void setExperienceTopListCache(Long pathId, Long id, Boolean status) {
        String key = RedisKeyEnum.CTC_TOP_EXPERIENCE_ZSET_CACHE.getKey(pathId.toString());
        if (status) {
            redisUtil.zadd(key, id.toString(), DateUtils.getCurrentTimeMillis(), RedisKeyEnum.CTC_TOP_EXPERIENCE_ZSET_CACHE.getExpireTime());
        } else {
            redisUtil.zremove(key, id.toString());
        }
    }

    private void deleteExperienceByCache(Long pathId, Long id) {
        // 删除星球置顶经验缓存
        String topListKey = RedisKeyEnum.CTC_TOP_EXPERIENCE_ZSET_CACHE.getKey(pathId.toString());
        redisUtil.zremove(topListKey, id.toString());
        // 删除星球推荐经验缓存
        String recommendListKey = RedisKeyEnum.CTC_RECOMMEND_EXPERIENCE_ZSET_CACHE.getKey(pathId.toString());
        redisUtil.zremove(recommendListKey, id.toString());
        // 删除热门列表
        String hotListKey = RedisKeyEnum.CTC_EXPERIENCE_HOT_SIGNS_CACHE.getKey(pathId.toString());
        redisUtil.zremove(hotListKey, id.toString());
        // 删除最新列表
        String newListKey = RedisKeyEnum.CTC_NEW_EXPERIENCE_LIST_CACHE.getKey(pathId.toString());
        redisUtil.zremove(newListKey, id.toString());
        // 刷新经验缓存
        refreshExperienceBaseCache(id);
        refreshCTCHomeExperience(id);
        pathService.refreshCTCPathDetailCacheByPathId(pathId);
    }

    private void refreshCTCHomeExperience(Long pathId) {
        PAIPageResponseBeanUtil<CTCExperienceListVO> experienceListPage = recommendPageList(pathId, 1, 10, null);
        List<CTCExperienceListVO> experienceListVOList = experienceListPage.getData();
        if (experienceListVOList == null) {
            experienceListVOList = new ArrayList<>();
        }
        String key = RedisKeyEnum.CTC_HOME_RECOMMEND_EXPERIENCE_CACHE.getKey(pathId.toString());
        redisUtil.set(key, JsonUtil.toJsonString(experienceListVOList), RedisKeyEnum.CTC_HOME_RECOMMEND_EXPERIENCE_CACHE.getExpireTime());
    }

    /**
     * 设置推荐经验缓存
     * @param pathId
     * @param id
     * @param status
     */
    private void setExperienceRecommendListCache(Long pathId, Long id, Boolean status) {
        String key = RedisKeyEnum.CTC_RECOMMEND_EXPERIENCE_ZSET_CACHE.getKey(pathId.toString());
        if (status) {
            redisUtil.zadd(key, id.toString(), DateUtils.getCurrentTimeMillis(), RedisKeyEnum.CTC_RECOMMEND_EXPERIENCE_ZSET_CACHE.getExpireTime());
        } else {
            redisUtil.zremove(key, id.toString());
        }
    }

    /**
     * 文章审核
     * @param experienceReviewRequest
     * @param userId
     */
    public void review(CCExperienceReviewRequestVO experienceReviewRequest, String userId) {
        Long id = experienceReviewRequest.getId();
        CCExperienceEntity experienceEntity = experienceEntityDAO.findById(id).orElse(null);
        Assert.isTrue(experienceEntity != null, "审核失败:经验不存在!");
        String oldStatus = experienceEntity.getStatus();
        String newStatus = experienceReviewRequest.getStatus();
        experienceEntity.setStatus(newStatus);
        experienceEntity.setUpdateBy(userId);
        experienceEntity.setUpdateDate(new Date());

        experienceEntityDAO.save(experienceEntity);

        CTCPathDetailVO pathDetailVO = pathService.getPathBaseFromCache(experienceEntity.getPathId());

        ThreadPoolExecutorUtil.execute(() -> {
            if (CommonStatusEnum.PUBLISHED.getStatus().equals(newStatus)) {
                this.addNewExperienceListCache(experienceEntity);
                if (userServiceClient.isAdmin(experienceEntity.getAuthor())) {
                    // 发布课程内容通知
                    AbstractResourceBehaviorService behaviorService = ResourceBehaviorFactory.getBehaviorService(ResourceBehaviorTypeEnum.FOCUS.getBehavior());
                    Set<String> focusUsers = behaviorService.getBehaviorUsersIdFromCache(ResourceTypeEnum.CTC_PATH.getType(), experienceEntity.getPathId());
                    if (CollectionUtils.isNotEmpty(focusUsers)) {
                        List<List<String>> focusUserList =  Lists.partition(new ArrayList<>(focusUsers), 200);
                        for (List<String> focusUserIdList : focusUserList) {
                            List<UserEntity> userEntities = userDAO.findByUserIdIn(focusUserIdList);
                            Set<UserEntity> focusWeixinUsers = userEntities.stream().filter(u -> StringUtils.isNotBlank(u.getWeixinOpenId())).collect(Collectors.toSet());
                        }
                    }
                }
            }
            if (CommonStatusEnum.PUBLISHED.getStatus().equals(oldStatus) && !CommonStatusEnum.PUBLISHED.getStatus().equals(newStatus)) {
                this.removeNewExperienceListCache(experienceEntity);
            }
        });

        /**
         * 消息通知
         */
        CommunityEventData communityEventData = new CommunityEventData();
        communityEventData.setEvent(CommonStatusEnum.PUBLISHED.getStatus().equals(newStatus) ? ESEventEnum.CREATE : ESEventEnum.DELETE);
        communityEventData.setType(CommunityDataTypeEnum.EXP.getType());
        communityEventData.setPathId(experienceEntity.getPathId());
        communityEventData.setBizId(experienceEntity.getId());
        communityEventData.setAuthor(experienceEntity.getAuthor());
        communityEventData.setTitle(experienceEntity.getTitle());
        communityEventData.setDescription(experienceEntity.getDescription());
        communityEventData.setCover(experienceEntity.getCover());
        communityEventData.setContent(HtmlToMarkdownConverter.convert(experienceEntity.getContent()));
        communityEventData.setSigns(experienceEntity.getSigns());
        communityEventData.setLastTime(experienceEntity.getCreateDate().getTime());
        CommunityContentEvent event = new CommunityContentEvent(communityEventData);
        applicationContext.publishEvent(event);
    }

    public void updateFree(Long id, String userId) {
        CCExperienceEntity experienceEntity = experienceEntityDAO.findById(id).orElse(null);
        Assert.isTrue(experienceEntity != null, "状态更新失败，资源不存在");
        boolean free = experienceEntity.getFree();
        experienceEntity.setFree(!free);
        experienceEntity.setUpdateBy(userId);
        experienceEntity.setUpdateDate(new Date());

        experienceEntityDAO.save(experienceEntity);

        ThreadPoolExecutorUtil.execute(() -> {
            refreshExperienceBaseCache(id);
        });
    }

    /**
     * 专区最新经验列表
     * @param pathId
     * @param pageNo
     * @param pageSize
     * @return
     */
    public PAIPageResponseBeanUtil<CTCExperienceListVO> newPageList(Long pathId, int pageNo, int pageSize, String userId) {
        String key = RedisKeyEnum.CTC_NEW_EXPERIENCE_LIST_CACHE.getKey(pathId.toString());
        if (!redisUtil.exists(key)) {
            this.refreshNewExperienceListCache(pathId);
        }
        Long total = redisUtil.zSetSize(key);
        Set<String> listCache = redisUtil.zreverseRange(key, (pageNo - 1) * pageSize, pageNo * pageSize);
        if (total == 0) {
            return PAIPageResponseBeanUtil.success(pageNo, pageSize, total, Collections.emptyList());
        }
        List<Long> expIdList = listCache.stream().map(Long::valueOf).collect(Collectors.toList());
        List<CTCExperienceBaseVO> experienceBaseVOList = batchGetExperienceBaseFromCache(expIdList);
        List<String> authors = experienceBaseVOList.stream().map(CTCExperienceBaseVO::getAuthor).collect(Collectors.toList());
        Map<String, UserBaseResponseInfoVO> authorMap = userServiceClient.batchGetUserInfoFromCache(authors);
        List<CTCExperienceListVO> experienceListVOList = new ArrayList<>();
        for (CTCExperienceBaseVO experienceBase : experienceBaseVOList) {
            experienceBase.setContent(null);
            CTCExperienceListVO experienceList = new CTCExperienceListVO();
            experienceList.setExperience(experienceBase);
            experienceList.setAuthor(authorMap.get(experienceBase.getAuthor()));
            experienceListVOList.add(experienceList);
        }
        return PAIPageResponseBeanUtil.success(pageNo, pageSize, total, experienceListVOList);
    }

    public PAIPageResponseBeanUtil<CTCExperienceListVO> recommendPageList(Long pathId, int pageNo, int pageSize, String userId) {
        String key = RedisKeyEnum.CTC_RECOMMEND_EXPERIENCE_ZSET_CACHE.getKey(pathId.toString());
        if (!redisUtil.exists(key)) {
            this.refreshRecommendExperienceListCache(pathId);
        }
        Long total = redisUtil.zSetSize(key);
        Set<String> listCache = redisUtil.zreverseRange(key, (pageNo - 1) * pageSize, pageNo * pageSize);
        if (total == 0) {
            return PAIPageResponseBeanUtil.success(pageNo, pageSize, total, Collections.emptyList());
        }
        List<Long> expIdList = listCache.stream().map(Long::valueOf).collect(Collectors.toList());
        List<CTCExperienceBaseVO> experienceBaseVOList = batchGetExperienceBaseFromCache(expIdList);
        List<String> authors = experienceBaseVOList.stream().map(CTCExperienceBaseVO::getAuthor).collect(Collectors.toList());
        Map<String, UserBaseResponseInfoVO> authorMap = userServiceClient.batchGetUserInfoFromCache(authors);
        List<CTCExperienceListVO> experienceListVOList = new ArrayList<>();
        for (CTCExperienceBaseVO experienceBase : experienceBaseVOList) {
            experienceBase.setContent(null);
            CTCExperienceListVO experienceList = new CTCExperienceListVO();
            experienceList.setExperience(experienceBase);
            experienceList.setAuthor(authorMap.get(experienceBase.getAuthor()));
            experienceListVOList.add(experienceList);
        }
        return PAIPageResponseBeanUtil.success(pageNo, pageSize, total, experienceListVOList);
    }

    public PAIPageResponseBeanUtil<CTCExperienceListVO> topPageList(Long pathId, int pageNo, int pageSize, String userId) {
        String key = RedisKeyEnum.CTC_TOP_EXPERIENCE_ZSET_CACHE.getKey(pathId.toString());
        if (!redisUtil.exists(key)) {
            this.refreshRecommendExperienceListCache(pathId);
        }
        Long total = redisUtil.zSetSize(key);
        Set<String> listCache = redisUtil.zreverseRange(key, (pageNo - 1) * pageSize, pageNo * pageSize);
        if (total == 0) {
            return PAIPageResponseBeanUtil.success(pageNo, pageSize, total, Collections.emptyList());
        }
        List<Long> expIdList = listCache.stream().map(Long::valueOf).collect(Collectors.toList());
        List<CTCExperienceBaseVO> experienceBaseVOList = batchGetExperienceBaseFromCache(expIdList);
        List<String> authors = experienceBaseVOList.stream().map(CTCExperienceBaseVO::getAuthor).collect(Collectors.toList());
        Map<String, UserBaseResponseInfoVO> authorMap = userServiceClient.batchGetUserInfoFromCache(authors);
        List<CTCExperienceListVO> experienceListVOList = new ArrayList<>();
        for (CTCExperienceBaseVO experienceBase : experienceBaseVOList) {
            experienceBase.setContent(null);
            CTCExperienceListVO experienceList = new CTCExperienceListVO();
            experienceList.setExperience(experienceBase);
            experienceList.setAuthor(authorMap.get(experienceBase.getAuthor()));
            experienceListVOList.add(experienceList);
        }
        return PAIPageResponseBeanUtil.success(pageNo, pageSize, total, experienceListVOList);
    }

    public PAIPageResponseBeanUtil<CTCExperienceDetailVO> adminPageList(String kw, Long pathId, List<String> statusList, int pageNo, int pageSize) {
        Pageable pageable = PageRequest.of(pageNo - 1, pageSize, Sort.by(Sort.Order.desc("id")));
        Specification<CCExperienceEntity> experienceEntitySpecification = buildExperienceQueryCondition(kw, pathId, statusList);
        Page<CCExperienceEntity> experienceEntityPage = experienceEntityDAO.findAll(experienceEntitySpecification, pageable);
        if (CollectionUtils.isEmpty(experienceEntityPage.getContent())) {
            return PAIPageResponseBeanUtil.success(pageNo, pageSize, experienceEntityPage.getTotalElements(), Collections.emptyList());
        }
        List<CCExperienceEntity> experienceEntities = experienceEntityPage.getContent();
        List<String> authors = experienceEntities.stream().map(CCExperienceEntity::getAuthor).collect(Collectors.toList());
        Map<String, UserBaseResponseInfoVO> authorMap = userServiceClient.batchGetUserInfoFromCache(authors);
        List<CTCExperienceDetailVO> experienceDetailVOList = experienceEntities.stream().map(exp -> {
            CTCExperienceDetailVO experienceDetailVO = new CTCExperienceDetailVO();
            CTCExperienceBaseVO experienceBase = convertExperienceBaseVO(exp);
            experienceBase.setContent(null);
            experienceBase.setDescription(null);
            experienceDetailVO.setExperience(experienceBase);
            experienceDetailVO.setAuthor(authorMap.get(exp.getAuthor()));
            experienceDetailVO.setPath(pathService.getCTCPathDetailVOFromCache(exp.getPathId()));
            return experienceDetailVO;
        }).collect(Collectors.toList());

        return PAIPageResponseBeanUtil.success(pageNo, pageSize, experienceEntityPage.getTotalElements(), experienceDetailVOList);
    }

    private Specification<CCExperienceEntity> buildExperienceQueryCondition(String kw, Long pathId, List<String> statusList) {
        Specification<CCExperienceEntity> specification = new Specification<CCExperienceEntity>() {
            @Override
            public Predicate toPredicate(Root<CCExperienceEntity> root, CriteriaQuery<?> criteriaQuery, CriteriaBuilder criteriaBuilder) {
                List<Predicate> predicates = new ArrayList<>();
                if (pathId != null) {
                    predicates.add(criteriaBuilder.equal(root.get("pathId").as(Long.class), pathId));
                }
                if (StringUtils.isNotBlank(kw)) {
                    predicates.add(
                            criteriaBuilder.or(
                                    criteriaBuilder.like(root.get("title").as(String.class), "%" + kw + "%")));
                }
                if (CollectionUtils.isNotEmpty(statusList)) {
                    CriteriaBuilder.In<String> in = criteriaBuilder.in(root.get("status"));
                    for(String status : statusList) {
                        in.value(status);
                    }
                    predicates.add(in);
                }
                Predicate[] pre = new Predicate[predicates.size()];
                criteriaQuery.where(predicates.toArray(pre));
                return criteriaQuery.getRestriction();
            }
        };
        return specification;
    }

    public CTCExperienceDetailVO getPermissionExperienceDetailFromCache(Long experienceId, String userId, String client) {
        CTCExperienceDetailVO exp = this.getExperienceDetailFromCache(experienceId, userId);
        if (ObjectUtils.equals(exp.getExperience().getFree(), true)) {
            return exp;
        }
        // 校验文章阅读权限
        pathService.validatePathPermission(userId, exp.getPath().getId());
        // 发布资源学习事件
        CourseStudyEvent event = new CourseStudyEvent(this, userId, ResourceTypeEnum.CTC_EXPERIENCE, experienceId);
        applicationContext.publishEvent(event);
        // 针对特殊客户端进行内容加工
        if (ClientEnum.WX_APP.name().equals(client)) {
            // 处理来自微信小程序端请求
            String html = exp.getExperience().getContent();
            String markdown = HtmlToMarkdownConverter.convert(html);
            if (StringUtils.isNotBlank(markdown)) {
                markdown = markdown.replaceAll("\\{#w-e-element-\\d+\\}", "")
                                   .replaceAll("\\{#w-e-text-\\d+\\}", "")
                                    .replaceAll("文章正文...", "")
                                    .replaceAll("链接文本链接地址", "")
                                    .replaceAll("↵确定↵", "")
                                    .replaceAll("↵", "\n");
                System.out.println(markdown);
                exp.getExperience().setContent(markdown);
            }
        }
        return exp;
    }

    /**
     * 查询经验详情
     * @param experienceId
     * @param userId
     * @return
     */
    public CTCExperienceDetailVO getExperienceDetailFromCache(Long experienceId, String userId) {
        CTCExperienceBaseVO experienceBase = this.getExperienceBaseFromCache(experienceId);
        if (experienceBase == null) {
            throw new BusinessException("文章不存在");
        }
        if (CommonStatusEnum.DELETED.getStatus().equals(experienceBase.getStatus())) {
            throw new BusinessException("文章已被删除");
        }
        if (!CommonStatusEnum.PUBLISHED.getStatus().equals(experienceBase.getStatus())) {
            if (!experienceBase.getAuthor().equals(userId)) {
                throw new BusinessException("文章已被下架");
            }
        }
        CTCExperienceDetailVO experienceDetail = new CTCExperienceDetailVO();
        experienceDetail.setExperience(experienceBase);
        experienceDetail.setAuthor(userServiceClient.getUserInfoByUserId(experienceBase.getAuthor()));
        experienceDetail.setPath(pathService.getPathBaseFromCache(experienceBase.getPathId(), userId));
        experienceDetail.setViewCount(addExperienceViewCountCacheAndDB(experienceId));
        experienceDetail.setPubTime(experienceBase.getCreateTime());
        AbstractResourceBehaviorService starService =
                ResourceBehaviorFactory.getBehaviorService(ResourceBehaviorTypeEnum.LIKE.getBehavior());
        Map<Long, Long> likeMap = starService.getBehaviorCountFromCache(ResourceTypeEnum.CTC_EXPERIENCE.getType(), Arrays.asList(experienceId));
        experienceDetail.setLikeCount(likeMap.getOrDefault(experienceId, 0L));
        if (StringUtils.isNotBlank(userId)) {
            Map<Long, Boolean> isLikeMap = starService.getIsBehaviorFromCache(ResourceTypeEnum.CTC_EXPERIENCE.getType(), Arrays.asList(experienceId), userId);
            experienceDetail.setIsLike(isLikeMap.getOrDefault(experienceId, false));
            AbstractResourceBehaviorService attendService =
                    ResourceBehaviorFactory.getBehaviorService(ResourceBehaviorTypeEnum.COLLECT.getBehavior());
            Map<Long, Boolean> isCollectMap = attendService.getIsBehaviorFromCache(ResourceTypeEnum.CTC_EXPERIENCE.getType(), Arrays.asList(experienceId), userId);
            experienceDetail.setIsCollect(isCollectMap.getOrDefault(experienceId, false));
        }
        boolean manager = userServiceClient.isAdmin(userId) || experienceBase.getAuthor().equals(userId);
        experienceDetail.setManager(manager);
        return experienceDetail;
    }

    /**
     * redis + MYSQL 双写记录浏览量
     * @param experienceId
     * @return
     */
    private Long addExperienceViewCountCacheAndDB(Long experienceId) {
        String key = RedisKeyEnum.CTC_EXPERIENCE_VIEW_COUNT_CACHE.getKey();
        String field = String.valueOf(experienceId);
        Long count = redisUtil.hincrex(key, field);
        if (count == 1) {
            CCExperienceEntity experience = experienceEntityDAO.findById(experienceId).orElse(null);
            count = experience.getViewCount() + 1;
            experience.setViewCount(count);
            experienceEntityDAO.save(experience);
            redisUtil.hset(key, field, String.valueOf(count), RedisKeyEnum.CTC_QUESTION_VIEW_COUNT_CACHE.getExpireTime());
        } else {
            ThreadPoolExecutorUtil.execute(() -> experienceEntityDAO.increxExperienceViewCount(experienceId));
        }
        return count;
    }

    public Long getViewCountFromCache(Long experienceId) {
        String key = RedisKeyEnum.CTC_EXPERIENCE_VIEW_COUNT_CACHE.getKey();
        String field = String.valueOf(experienceId);
        String count = redisUtil.hget(key, field);
        if (StringUtils.isNotBlank(count)) {
            return Long.parseLong(count);
        }
        return addExperienceViewCountCacheAndDB(experienceId);
    }

    public Map<Long, Long> batchGetViewCountFromCache(List<Long> experienceIds) {
        Map<Long, Long> countMap = new HashMap<>();
        String key = RedisKeyEnum.CTC_EXPERIENCE_VIEW_COUNT_CACHE.getKey();
        List<String> fields = experienceIds.stream().map(String::valueOf).collect(Collectors.toList());
        List<String> counts = redisUtil.hmget(key, fields);
        for (int i = 0; i < experienceIds.size(); i++) {
            Long id = experienceIds.get(i);
            String count = counts.get(i);
            if (StringUtils.isNotBlank(count)) {
                countMap.put(id, Long.parseLong(count));
            } else {
                countMap.put(id, getExperienceCountFromCache(id));
            }
        }
        return countMap;
    }

    public List<CTCExperienceBaseVO> batchGetExperienceBaseFromCache(List<Long> expIdList) {
        if (CollectionUtils.isEmpty(expIdList)) {
            return Collections.emptyList();
        }
        List<CTCExperienceBaseVO> experienceBaseList = new ArrayList<>();
        List<String> expCacheKeys = expIdList.stream().map(String::valueOf).collect(Collectors.toList());
        List<String> expCaches = redisUtil.mget(expCacheKeys);
        for (int i = 0; i < expIdList.size(); i++) {
            Long expId = expIdList.get(i);
            String cache = expCaches.get(i);
            CTCExperienceBaseVO experienceBase;
            if (StringUtils.isNotBlank(cache)) {
                experienceBase = JsonUtil.fromJson(cache, CTCExperienceBaseVO.class);
            } else {
                experienceBase = refreshExperienceBaseCache(expId);
            }
            experienceBaseList.add(experienceBase);
        }
        return experienceBaseList.stream().filter(Objects::nonNull).collect(Collectors.toList());
    }

    public Map<Long, CTCExperienceBaseVO> batchGetExperienceBaseMapFromCache(List<Long> expIdList) {
        if (CollectionUtils.isEmpty(expIdList)) {
            return Collections.emptyMap();
        }
        Map<Long, CTCExperienceBaseVO> map = new HashMap<>();
        List<String> expCacheKeys = expIdList.stream().map(String::valueOf).collect(Collectors.toList());
        List<String> expCaches = redisUtil.mget(expCacheKeys);
        for (int i = 0; i < expIdList.size(); i++) {
            Long expId = expIdList.get(i);
            String cache = expCaches.get(i);
            CTCExperienceBaseVO experienceBase;
            if (StringUtils.isNotBlank(cache)) {
                experienceBase = JsonUtil.fromJson(cache, CTCExperienceBaseVO.class);
            } else {
                experienceBase = refreshExperienceBaseCache(expId);
            }
            if (experienceBase != null) {
                map.put(expId, experienceBase);
            }
        }
        return map;
    }

    private void addNewExperienceListCache(CCExperienceEntity experienceEntity) {
        String key = RedisKeyEnum.CTC_NEW_EXPERIENCE_LIST_CACHE.getKey(experienceEntity.getPathId().toString());
        if (redisUtil.exists(key)) {
            redisUtil.zadd(key, JsonUtil.toJsonString(experienceEntity.getId()), experienceEntity.getCreateDate().getTime(),
                    RedisKeyEnum.CTC_NEW_EXPERIENCE_LIST_CACHE.getExpireTime());
            return;
        }
        this.refreshNewExperienceListCache(experienceEntity.getPathId());
    }

    /**
     * 查询经验数量
     * @param pathId
     * @return
     */
    public Long getExperienceCountFromCache(Long pathId) {
        String key = RedisKeyEnum.CTC_NEW_EXPERIENCE_LIST_CACHE.getKey(pathId.toString());
        if (redisUtil.exists(key)) {
            refreshNewExperienceListCache(pathId);
        }
        return redisUtil.zsize(key);
    }

    private void removeNewExperienceListCache(CCExperienceEntity experienceEntity) {
        String key = RedisKeyEnum.CTC_NEW_EXPERIENCE_LIST_CACHE.getKey(experienceEntity.getPathId().toString());
        if (redisUtil.exists(key)) {
            redisUtil.zremove(key, JsonUtil.toJsonString(experienceEntity.getId()));
            return;
        }
        this.refreshNewExperienceListCache(experienceEntity.getPathId());
    }

    private void refreshNewExperienceListCache(Long pathId) {
        List<CCExperienceEntity> experienceEntities = experienceEntityDAO.findAllByPathIdAndStatusOrderByIdDesc(pathId, CommonStatusEnum.PUBLISHED.getStatus());
        if (CollectionUtils.isEmpty(experienceEntities)) {
            return;
        }
        String key = RedisKeyEnum.CTC_NEW_EXPERIENCE_LIST_CACHE.getKey(pathId.toString());
        Set<ZSetOperations.TypedTuple<String>> tuples = experienceEntities.stream().map(
                exp -> {
                    String value = String.valueOf(exp.getId());
                    Double score = Double.valueOf(exp.getCreateDate().getTime());
                    return new DefaultTypedTuple<>(value, score);
                }).collect(Collectors.toSet());
        redisUtil.zadd(key, tuples,RedisKeyEnum.CTC_NEW_EXPERIENCE_LIST_CACHE.getExpireTime());
    }

    private void refreshRecommendExperienceListCache(Long pathId) {
        List<CCExperienceEntity> experienceEntities = experienceEntityDAO.findAllByPathIdAndRecommendAndStatusOrderByIdDesc(pathId, true, CommonStatusEnum.PUBLISHED.getStatus());
        if (CollectionUtils.isEmpty(experienceEntities)) {
            return;
        }
        String key = RedisKeyEnum.CTC_RECOMMEND_EXPERIENCE_ZSET_CACHE.getKey(pathId.toString());
        Set<ZSetOperations.TypedTuple<String>> tuples = experienceEntities.stream().map(
                exp -> {
                    String value = String.valueOf(exp.getId());
                    Double score = Double.valueOf(exp.getRecommendDate().getTime());
                    return new DefaultTypedTuple<>(value, score);
                }).collect(Collectors.toSet());
        redisUtil.zadd(key, tuples,RedisKeyEnum.CTC_RECOMMEND_EXPERIENCE_ZSET_CACHE.getExpireTime());
    }

    private void refreshTopExperienceListCache(Long pathId) {
        List<CCExperienceEntity> experienceEntities = experienceEntityDAO.findAllByPathIdAndTopAndStatusOrderByIdDesc(pathId, true, CommonStatusEnum.PUBLISHED.getStatus());
        if (CollectionUtils.isEmpty(experienceEntities)) {
            return;
        }
        String key = RedisKeyEnum.CTC_TOP_EXPERIENCE_ZSET_CACHE.getKey(pathId.toString());
        Set<ZSetOperations.TypedTuple<String>> tuples = experienceEntities.stream().map(
                exp -> {
                    String value = String.valueOf(exp.getId());
                    Double score = Double.valueOf(exp.getTopDate().getTime());
                    return new DefaultTypedTuple<>(value, score);
                }).collect(Collectors.toSet());
        redisUtil.zadd(key, tuples,RedisKeyEnum.CTC_TOP_EXPERIENCE_ZSET_CACHE.getExpireTime());
    }

    public CTCExperienceBaseVO getExperienceBaseFromCache(Long experienceId) {
        String key = RedisKeyEnum.CTC_EXPERIENCE_BASE_CACHE.getKey(experienceId.toString());
        String cache = redisUtil.get(key);
        if (StringUtils.isNotBlank(cache)) {
            return JsonUtil.fromJson(cache, CTCExperienceBaseVO.class);
        }
        return refreshExperienceBaseCache(experienceId);
    }

    private CTCExperienceBaseVO refreshExperienceBaseCache(Long experienceId) {
        CCExperienceEntity experienceEntity = experienceEntityDAO.findById(experienceId).orElse(null);
        return refreshExperienceBaseCache(experienceEntity);
    }

    private CTCExperienceBaseVO refreshExperienceBaseCache(CCExperienceEntity experienceEntity) {
        if (experienceEntity == null) {
            return null;
        }
        Long id = experienceEntity.getId();
        String key = RedisKeyEnum.CTC_EXPERIENCE_BASE_CACHE.getKey(id.toString());
        CTCExperienceBaseVO experienceBase = convertExperienceBaseVO(experienceEntity);
        redisUtil.set(key, JsonUtil.toJsonString(experienceBase), RedisKeyEnum.CTC_EXPERIENCE_BASE_CACHE.getExpireTime());
        return experienceBase;
    }

    private CTCExperienceBaseVO convertExperienceBaseVO(CCExperienceEntity experienceEntity) {
        CTCExperienceBaseVO experienceBaseVO = new CTCExperienceBaseVO();
        BeanUtils.copyProperties(experienceEntity, experienceBaseVO);
        if (StringUtils.isNotBlank(experienceEntity.getSigns())) {
            experienceBaseVO.setSignList(ListUtil.string2List(experienceEntity.getSigns()));
        }
        if (experienceEntity.getTopDate() != null) {
            experienceBaseVO.setTopTime(experienceEntity.getTopDate().getTime());
        }
        if (experienceEntity.getRecommendDate() != null) {
            experienceBaseVO.setRecommendTime(experienceEntity.getRecommendDate().getTime());
        }
        experienceBaseVO.setCreateTime(experienceEntity.getCreateDate().getTime());
        List<String> images = new ArrayList<>();
        if (StringUtils.isNotBlank(experienceEntity.getCover())) {
            images.add(experienceEntity.getCover());
        }
        images.addAll(ArticleUtil.getImagesFromHtml(experienceEntity.getContent()));
        experienceBaseVO.setImages(images);
        return experienceBaseVO;
    }

    private CCExperienceEntity convert(CCExperienceSaveRequestVO experienceSaveRequest, String userId) {
        Date now = new Date();
        CCExperienceEntity experienceEntity;
        Long id = experienceSaveRequest.getId();
        if (id != null) {
            experienceEntity = experienceEntityDAO.findById(id).orElse(null);
            Assert.isTrue(experienceEntity != null, "编辑失败:经验不存在");
            Assert.isTrue(experienceEntity.getAuthor().equals(userId), "无权限");
        } else {
            experienceEntity = new CCExperienceEntity();
            experienceEntity.setPathId(experienceSaveRequest.getPathId());
            experienceEntity.setAuthor(userId);
            experienceEntity.setStatus(CommonStatusEnum.NEW.getStatus());
            experienceEntity.setViewCount(0L);
            experienceEntity.setTop(false);
            experienceEntity.setFree(false);
            experienceEntity.setRecommend(false);
            experienceEntity.setCreateBy(userId);
            experienceEntity.setCreateDate(now);
        }
        experienceEntity.setTitle(experienceSaveRequest.getTitle());
        experienceEntity.setCover(experienceSaveRequest.getCover());
        if (StringUtils.isNotBlank(experienceSaveRequest.getDescription())) {
            experienceEntity.setDescription(experienceSaveRequest.getDescription());
        } else {
            String textContent = HtmlUtil.getTxtContent(experienceSaveRequest.getContent());
            // 截取正文前100个字作为概要
            String desc = textContent != "" ? textContent.substring(0, Math.min(100, textContent.length())) : "";
            experienceEntity.setDescription(desc);
        }
        experienceEntity.setContent(experienceSaveRequest.getContent());
        experienceEntity.setSigns(experienceSaveRequest.getSigns());
        experienceEntity.setUpdateBy(userId);
        experienceEntity.setUpdateDate(now);
        return experienceEntity;
    }

    @Override
    public Map<String, MessageResourceVO> batchGetMessResource(List<String> resourceIdList) {
        Map<String, MessageResourceVO> map = new HashMap<>();
        List<Long> ids = resourceIdList.stream().map(Long::valueOf).collect(Collectors.toList());
        Map<Long, CTCExperienceBaseVO> questionBaseVOMap = batchGetExperienceBaseMapFromCache(ids);
        for (String resourceId : resourceIdList) {
            CTCExperienceBaseVO exp = questionBaseVOMap.get(Long.valueOf(resourceId));
            if (exp != null) {
                MessageResourceVO messageResource = new MessageResourceVO();
                messageResource.setId(resourceId);
                messageResource.setTitle(exp.getTitle());
                messageResource.setCover(exp.getCover());
                messageResource.setAuthor(exp.getAuthor());
                messageResource.setModule(ResourceTypeEnum.CTC_EXPERIENCE.getTypeName());
                messageResource.setType(ResourceTypeEnum.CTC_EXPERIENCE.getType());
                map.put(resourceId, messageResource);
            }
        }
        return map;
    }

    @Override
    public DataCountResponseVO monitorDataCount() {
        return null;
    }

    @Override
    public ResourceBaseInfoVO getResourceBaseInfo(Long resourceId) {
        CTCExperienceBaseVO exp = getExperienceBaseFromCache(resourceId);
        if (exp == null) {
            return null;
        }
        return ResourceBaseInfoVO.of(resourceId, exp.getAuthor(), exp.getTitle(), null);
    }

    @Override
    public List<DtoRequest> getSyncResourcesByDate(Date start, Date endDate) {
        return null;
    }
}
